//
//  NMSettingController.swift
//  NetEaseMusic
//
//  Created by SAGESSE on 2020/5/19.
//  Copyright © 2020 SAGESSE. All rights reserved.
//

import UIKit
import WebKit


@IBDesignable class NMSettingCell: UITableViewCell {
    
    @IBInspectable var action: String? {
        willSet {
            // Ignore when the value not any changes.
            guard newValue != action else {
                return
            }
            // Create a new connection.
            connection = newValue.flatMap {
                return NMSetting($0)
            }
            connection?.didSet { [unowned self] in
                self.refresh(true)
            }
            // Manual refresh UI.
            refresh()
        }
    }
    
    var topSeparatorFrame: CGRect {
        return .zero
    }
    var bottomSeparatorFrame: CGRect {
        if super.responds(to: #selector(_separatorFrame)) {
            let rect = super._separatorFrame()
            guard rect.minX != 0 else {
                return .zero
            }
            return rect
        }
        return .zero
    }

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        self.setup()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.setup()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        self.setup()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        // Readjus detail text label when style is not subtitle.
        if let detailTextLabel = detailTextLabel, style() != .subtitle {
            detailTextLabel.center.y = contentView.bounds.size.height / 2
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        
        // Ignore when reference is nil
        guard let connection = connection, connection.reference != nil else {
            return
        }
        switch connection.function {
        case .mark:
            // When event specified parameter, it is set to the specified value when tap.
            connection.value = connection.parameters

        case .tap:
            // When event specified parameter, it is set to the specified value when tap.
            connection.parameters.map {
                connection.value = $0
            }
            // Update source info when event taped.
            connection.store["source-title"] = textLabel?.text
            connection.store["source-action"] = connection.key

        default:
            break
        }
    }
    
    override func _topSeparatorFrame() -> CGRect {
        return topSeparatorFrame
    }
    override func _separatorFrame() -> CGRect {
        return bottomSeparatorFrame
    }
    
    
    @objc
    private func toggle(_ sender: UISwitch) {
        
        if connection?.function == .sw {
            connection?.value = (sender.isOn ? "on" : "off")
        }
    }

    
    private func refresh(_ animated: Bool = false) {
        var accessoryView = self.accessoryView
        var selectionStyle = self.selectionStyle
        var accessoryType = self.accessoryType

        switch connection?.function {
        case .tap:
            accessoryType = .disclosureIndicator
            selectionStyle = .none
            accessoryView = nil
            
            if connection?.reference != nil {
                detailTextLabel?.text = connection?.key.map(NMSetting.localizedValue)
                setNeedsLayout()
            }

        case .sw:
            accessoryType = .disclosureIndicator
            selectionStyle = .none
            accessoryView = switchView {
                $0.setOn((connection?.value == "on"), animated: animated)
            }

        case .mark:
            accessoryType = .none
            selectionStyle = .none
            accessoryView = nil
            if connection?.value == connection?.parameters {
                accessoryType = .checkmark
            }

        case .none:
            accessoryType = .none
            selectionStyle = .default
            accessoryView = nil
        }
        
        if self.accessoryView != accessoryView {
            self.accessoryView = accessoryView
        }
        if self.selectionStyle != selectionStyle {
            self.selectionStyle = selectionStyle
        }
        if self.accessoryType != accessoryType {
            self.accessoryType = accessoryType
        }
    }
    private func switchView(_ update: (UISwitch) -> ()) -> UISwitch {
        
        if let view = accessoryView as? UISwitch {
            update(view)
            return view
        }
        
        let view = UISwitch()
        view.addTarget(self, action: #selector(toggle), for: .valueChanged)
        update(view)
        return view
    }
    
    private func setup() {
        
        textLabel?.font = UIFont.systemFont(ofSize: 16)
        textLabel?.textColor = rgba(0xcc000001)

        switch style() {
        case .subtitle:
            detailTextLabel?.font = UIFont.systemFont(ofSize: 11)
            detailTextLabel?.textColor = rgba(0x66000001)
            
        default:
            detailTextLabel?.font = UIFont.systemFont(ofSize: 14)
            detailTextLabel?.textColor = rgba(0x66000001)
            
        }
    }

    private var connection: NMSetting?
}
@IBDesignable class NMSettingTable: UITableView {
    
    override var separatorColor: UIColor? {
        set { return _separatorColor = newValue }
        get { return _separatorColor ?? rgba(0x19010101) }
    }
    
    private var _separatorColor: UIColor?
}
@IBDesignable class NMSettingController: UITableViewController {

    @IBInspectable var filter: String?
    
    // 0x07010101
    override func viewDidLoad() {
        super.viewDidLoad()
        
        title.map {
            title = NMSetting.eval($0)
        }
        tabBarItem.title.map {
            tabBarItem.title = NMSetting.eval($0)
        }
        navigationItem.title.map {
            navigationItem.title = NMSetting.eval($0)
        }
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // Hit cells caches in section.
        if let cells = filtering[section] {
            return cells.count
        }
        // Get the original cell count in section.
        let count = super.tableView(tableView, numberOfRowsInSection: section)
        // Ignore when filter is not specified.
        guard let parts = filter?.components(separatedBy: ","), !parts.isEmpty else {
            return count
        }
        var cells = [IndexPath]()
        for row in 0 ..< count {
            let indexPath = IndexPath(row: row, section: section)
            let cell = super.tableView(tableView, cellForRowAt: indexPath)
            // Ignore when the action match in filter.
            if let action = (cell as? NMSettingCell)?.action, parts.contains(NMSetting.eval(action)) {
                continue
            }
            // The cell is normal.
            cells.append(indexPath)
        }
        filtering[section] = cells
        return cells.count
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // When the filtering is active use it.
        if let indexPaths = filtering[indexPath.section] {
            return super.tableView(tableView, cellForRowAt: indexPaths[indexPath.row])
        }
        return super.tableView(tableView, cellForRowAt: indexPath)
    }

    
    override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        // If the section footer has content, the size is automatically calculated.
        guard let title = super.tableView(tableView, titleForFooterInSection: section), !title.isEmpty else {
            return 0.000001
        }
        return super.tableView(tableView, heightForFooterInSection: section)
    }

    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        // If the section header has content, the size is automatically calculated
        guard let title = super.tableView(tableView, titleForHeaderInSection: section), !title.isEmpty else {
            return 8
        }
        return 43
    }
    
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        // When the filtering is active use it.
        if let indexPaths = filtering[indexPath.section] {
            return super.tableView(tableView, heightForRowAt: indexPaths[indexPath.row])
        }
        return super.tableView(tableView, heightForRowAt: indexPath)
    }

    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }

//    override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//        let v = super.tableView(tableView, viewForHeaderInSection: section)
//        return v
//    }
    
    
    private var filtering: [Int: [IndexPath]] = [:]
}


// MARK: -


class NMSetting {
    
    /// Bind the store key and value type.
    typealias Store = NMDataStore<String, String>
    
    /// Bind the function to connection.
    enum Function: String {
        case sw
        case tap
        case mark
    }
    
    /// Create a connection by format "function:name:parameters"
    init?(_ cmd: String) {
        // Prase the command to items..
        var items = Self.eval(cmd).components(separatedBy: ":")
        guard !items.isEmpty else {
            return nil
        }
        // Check that the function is valid.
        guard let function = Function(rawValue: items.removeFirst()) else {
            return nil
        }
        self.function = function
        self.reference = Self.removeFirst(&items).map(Self.store.reference)
        self.parameters = items.first
    }
    
    let function: Function
    let reference: Store.Reference?
    let parameters: String?
    
    var store: Store {
        return Self.store
    }
    var key: String? {
        return reference?.key
    }
    var value: String? {
        set { reference?.value = newValue }
        get { reference?.value }
    }
    
    
    func willSet(_ handler: @escaping () -> ()) {
        reference?.willSet(handler)
    }
    func willSet(_ handler: @escaping (String?) -> ()) {
        reference?.willSet(handler)
    }
    
    func didSet(_ handler: @escaping () -> ()) {
        reference?.didSet(handler)
    }
    func didSet(_ handler: @escaping (String?) -> ()) {
        reference?.didSet(handler)
    }
    
    
    static func eval(_ value: String) -> String {
        
        var ranges: [NSRange] = []
        eval?.enumerateMatches(in: value, range: .init(location: 0, length: value.count)) { result, _, _ in
            guard let range = result?.range(at: 1), range.location != NSNotFound else {
                return
            }
            ranges.append(range)
        }
        
        // When the ranges is empty, no values that can change.
        guard !ranges.isEmpty else {
            return value
        }
        let newValue = NSMutableString(string: value)
        ranges.reversed().forEach {
            guard let value = Self.store[newValue.substring(with: $0)] else {
                return
            }
            newValue.replaceCharacters(in: NSMakeRange($0.location - 1, $0.length + 2),
                                       with: value)
        }
        return newValue as String
    }
    
    static func value(for key: String) -> String? {
        return store[key]
    }
    static func localizedValue(for key: String) -> String {
        // Ignore when the value not found for key.
        guard let value = store[key] else {
            return key
        }
        guard !value.isEmpty else {
            return value
        }
        
        return Self.bundle.localizedString(forKey: "\(key):\(value)", value: value, table: "Setting")
    }

    static func setValue(_ newValue: String?, for key: String) {
        store[key] = newValue
    }

    private static func loadStore() -> Store {
        let store = Store()
        configure(store)
        return store
    }
    private static func removeFirst(_ items: inout [String]) -> String? {
        if items.isEmpty {
            return nil
        }
        return items.removeFirst()
    }
    
    private static let eval = try? NSRegularExpression(pattern: "\\{([^}]+)\\}")
    private static let store = loadStore()
}


// MARK: -


extension NMSetting: Logport {
  
    static func configure(_ store: Store) {
        // Rebind to unowned store.
        unowned let store = store
        
        // Remove all stored items.
        store.clean()
        
        // Import default item into store.
        Self.bundle.url(forResource: "Setting", withExtension: "plist").map {
            NSDictionary(contentsOf: $0)?.forEach {
                if let key = $0 as? String {
                    store[key] = $1 as? String
                }
            }
        }
        // Import user item into store.
        // Self.sqlite.query<T>.forEach {
        //     $0.
        // }
        // key,value,sync

        // Export user item into sqlite.
        store.sync {
            logger.trace?.write("export \"\($0)\" = \"\($1 ?? "<null>")\"")
            
            // Self.sqlite.update
        }
        
        #if TARGET_INTERFACE_BUILDER
        store["source-action"] = "setting.privacy.message"
        #endif
        
        // When `setting.music.auto-cache.*` turned to on, the `setting.music.auto-cache` will be turned on also.
        store.get("setting.music.auto-cache") {
            store.or4sw(
                "setting.music.auto-cache.free",
                "setting.music.auto-cache.vip",
                "setting.music.auto-cache.favorite"
            )
        }
        
        // When item `setting.music.auto-cache.favorite` turned to on, the `setting.music.auto-cache.free` and `setting.music.auto-cache.vip` will be turned on also.
        store.didSet("setting.music.auto-cache.favorite") {
            if store.or4sw("setting.music.auto-cache.favorite") == "on" {
                store["setting.music.auto-cache.free"] = "on"
                store["setting.music.auto-cache.vip"] = "on"
            }
            // Sync each change into auto-cache item.
            store["setting.music.auto-cache"] = nil
        }
        
        store.didSet("setting.music.auto-cache.free", "setting.music.auto-cache.vip") {
            if store.or4sw("setting.music.auto-cache.free", "setting.music.auto-cache.vip") == "off" {
                store["setting.music.auto-cache.favorite"] = "off"
            }
            // Sync each change into auto-cache item.
            store["setting.music.auto-cache"] = nil
        }
        
        
        store.get("setting.cache.music") {
            bytes(0)
        }
        store.set("setting.cache.music") {
            logger.debug?.write($0 ?? "")
        }
        
        store.get("setting.cache.image") {
            bytes(0)
        }
        store.set("setting.cache.image") {
            logger.debug?.write($0 ?? "")
        }
        
        store.set("setting.music.close.countdown") {}
        store.get("setting.music.close.countdown") {
            // When alarm is closed, hiden countdown text.
            guard let closing = closing, closing.remaining > 0 else {
                return ""
            }
            let s = Int(closing.remaining) % 60
            let m = Int(closing.remaining) / 60
            return String(format: "{img:cm4_list_icn_time} %02zd:%02zd", m, s) // {img:xxx} 00:00
        }
        
        store.get("setting.music.close") {
            // When alarm is closed, the options is disable.
            guard let value = closing?.extra as? String else {
                return "disable"
            }
            guard !value.hasPrefix("custom") else {
                return "custom"
            }
            return value
        }
        store.set("setting.music.close") {
            switch $0 {
            case "custom":
                // When options is custom, show the time picker.
                DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
                    store["setting.music.close"] = "custom-80"
                }

            case .some(let value):
                // Reset the timer when value is specified.
                closing?.invalidate()
                closing = nil
                
                // Create a notify publisher.
                let ntf = store.reference(for: "setting.music.close.countdown")
                defer {
                    ntf.value = nil
                }

                // Remove custom value prefix.
                let start = value.range(of: "custom-")?.upperBound ?? value.startIndex
                guard let seconds = TimeInterval(value[start ..< value.endIndex]) else {
                    return
                }

                // Create a new countdown timer.
                closing = .countdown(seconds, with: value) {
                    // Notify user countdown timer remaining.
                    ntf.value = nil
                    
                    // When remaining is over reset timer.
                    guard $0 <= 0 else {
                        return
                    }
                    
                    // Reset the closing timer.
                    store["setting.music.close"] = "disable"
                }

            case .none:
                break
            }
        }
    }
    
    static func bytes(_ value: Double) -> String {
        let step = 1024.0
        let levels = ["K", "M", "G", "T", "P"]
        
        var level = 0
        var remaining = value / step
        while remaining > step && level < levels.count - 1 {
            remaining /= step
            level += 1
        }
        
        return String(format: "%.0lf%@", remaining, levels[level])
    }
    
//    static func numberOfCache(_ path: String) -> Double {
//
//        let manager = FileManager.default
//        guard manager.fileExists(atPath: path) else {
//            return 0
//        }
//
//        var size = 0.0
//        manager.subpaths(atPath: path)?.forEach {
//            (try? manager.attributesOfItem(atPath: "\(path)/\($0)")).map {
//                size += $0[.size] as? Double ?? 0
//            }
//        }
//        return size
//    }
    
    /// An alarm clock that is timed to close.
    private static var closing: NMTimer?
    
    /// Fetch current setting bundle.
    private static var bundle: Bundle {
        #if TARGET_INTERFACE_BUILDER
        return Bundle(for: Self.self)
        #else
        return Bundle.main
        #endif
    }
}

extension NMDataStore where Value == String {
    
    func or4sw(_ keys: Key...) -> Value {
        for key in keys {
            if self[key] == "on" {
                return "on"
            }
        }
        return "off"
    }
    func and4sw(_ keys: Key...) -> Value {
        for key in keys {
            if self[key] != "on" {
                return "off"
            }
        }
        return "on"
    }
}


// MARK: -


fileprivate extension UITableViewCell {
    
    /// Export the style.
    @available(iOS, introduced: 9.0, deprecated: 13.4.1, message: "Please check the availability of the latest version.")
    @NSManaged func style() -> UITableViewCell.CellStyle

    /// Export the separator frame.
    @available(iOS, introduced: 9.0, deprecated: 13.4.1, message: "Please check the availability of the latest version.")
    @NSManaged func _topSeparatorFrame() -> CGRect
    /// Export the top separator frame.
    @available(iOS, introduced: 9.0, deprecated: 13.4.1, message: "Please check the availability of the latest version.")
    @NSManaged func _separatorFrame() -> CGRect
}


